分类
联系方式
  1. 新浪微博
  2. E-mail

QuteBrowser Mode 模式管理架构实现

介绍

QuteBrowser 像 Vim 一样,也包含多种 Mode,modman 是 QuteBrowser 中 Mode 管理器。本文梳理 modman 的实现原理。

多少种 Mode?

参见 KeyMode 枚举:

class KeyMode(enum.Enum):

    """Key input modes."""

    normal = enum.auto()  #: Normal mode (no mode was entered)
    hint = enum.auto()  #: Hint mode (showing labels for links)
    command = enum.auto()  #: Command mode (after pressing the colon key)
    yesno = enum.auto()  #: Yes/No prompts
    prompt = enum.auto()  #: Text prompts
    insert = enum.auto()  #: Insert mode (passing through most keys)
    passthrough = enum.auto()  #: Passthrough mode (passing through all keys)
    caret = enum.auto()  #: Caret mode (moving cursor with keys)
    set_mark = enum.auto()
    jump_mark = enum.auto()
    record_macro = enum.auto()
    run_macro = enum.auto()
    # 'register' is a bit of an oddball here: It's not really a "real" mode,
    # but it's used in the config for common bindings for
    # set_mark/jump_mark/record_macro/run_macro.
    register = enum.auto()

在不同的 Mode 下,键盘的输入行为是不一样的。

Qt 键盘事件响应

在 app.py 中的 init 中,对事件过滤器进行初始化:

eventfilter.init()

具体实现:

def init() -> None:
    """Initialize the global EventFilter instance."""
    event_filter = EventFilter(parent=objects.qapp)
    event_filter.install()
    quitter.instance.shutting_down.connect(event_filter.shutdown)

EventFilter

这是一个 Qt 对象,注册进入 QApp 中,接管了用户按键:

class EventFilter(QObject):

    """Global Qt event filter.

    Attributes:
        _activated: Whether the EventFilter is currently active.
        _handlers: A {QEvent.Type: callable} dict with the handlers for an
                   event.
    """

    def __init__(self, parent: QObject = None) -> None:
        super().__init__(parent)
        self._activated = True
        self._handlers = {
            QEvent.KeyPress: self._handle_key_event,
            QEvent.KeyRelease: self._handle_key_event,
            QEvent.ShortcutOverride: self._handle_key_event,
        }

    def install(self) -> None:
        objects.qapp.installEventFilter(self)

_handle_key_event

该类还有一个 eventFilter 方法,用于响应事件,最终都会调入 _handle_key_event:

def _handle_key_event(self, event: QKeyEvent) -> bool:
    """Handle a key press/release event.

    Args:
        event: The QEvent which is about to be delivered.

    Return:
        True if the event should be filtered, False if it's passed through.
    """
    active_window = objects.qapp.activeWindow()
    if active_window not in objreg.window_registry.values():
        # Some other window (print dialog, etc.) is focused so we pass the
        # event through.
        return False
    try:
        man = modeman.instance('current')
        return man.handle_event(event)
    except objreg.RegistryUnavailableError:
        # No window available yet, or not a MainWindow
        return False

由于每个 Window 都有一个 ModeManager,因此从中取出活跃的那个,把 event 分发进去。

ModeManager

键盘模式管理器,一个类。

属性

  • mode:当前所属模式
  • hintmanager:与当前窗口关联的提示管理器
  • _win_id:窗口 id
  • _prev_mode:prompt 弹出前的模式
  • parsers:字典,mode 与 keyparsers 的映射

handle_event

根据当前设置的模式,在模式内进行分发:

def handle_event(self, event: QEvent) -> bool:
    """Filter all events based on the currently set mode.

    Also calls the real keypress handler.

    Args:
        event: The KeyPress to examine.

    Return:
        True if event should be filtered, False otherwise.
    """
    handlers: Mapping[QEvent.Type, Callable[[QKeyEvent], bool]] = {
        QEvent.KeyPress: self._handle_keypress,
        QEvent.KeyRelease: self._handle_keyrelease,
        QEvent.ShortcutOverride:
            functools.partial(self._handle_keypress, dry_run=True),
    }
    handler = handlers[event.type()]
    return handler(cast(QKeyEvent, event))

_handle_keypress

响应按下事件,这是分发的最关键之处:

def _handle_keypress(self, event: QKeyEvent, *,
                     dry_run: bool = False) -> bool:
    """Handle filtering of KeyPress events.
      处理键盘事件过滤

    Args:
        event: The KeyPress to examine. 事件
        dry_run: Don't actually handle the key, only filter it 是否消费事件.

    Return:
        True if event should be filtered, False otherwise. 是否消费事件
    """
    # 获取当前 Mode
    curmode = self.mode
    # 获取当前 Mode 对应的 Parser
    parser = self.parsers[curmode]
    if curmode != usertypes.KeyMode.insert:
        log.modes.debug("got keypress in mode {} - delegating to "
                        "{}".format(curmode, utils.qualname(parser)))
    # 由 Parser 是否匹配该事件
    match = parser.handle(event, dry_run=dry_run)

    # 是否是功能键
    has_modifier = event.modifiers() not in [
        Qt.NoModifier,
        Qt.ShiftModifier,
    ]  # type: ignore[comparison-overlap]
    is_non_alnum = has_modifier or not event.text().strip()

    forward_unbound_keys = config.cache['input.forward_unbound_keys']

    if match:
        filter_this = True
    elif (parser.passthrough or forward_unbound_keys == 'all' or
          (forward_unbound_keys == 'auto' and is_non_alnum)):
        filter_this = False
    else:
        filter_this = True

    if not filter_this and not dry_run:
        self._releaseevents_to_pass.add(KeyEvent.from_event(event))

    if curmode != usertypes.KeyMode.insert:
        focus_widget = objects.qapp.focusWidget()
        log.modes.debug("match: {}, forward_unbound_keys: {}, "
                        "passthrough: {}, is_non_alnum: {}, dry_run: {} "
                        "--> filter: {} (focused: {!r})".format(
                            match, forward_unbound_keys,
                            parser.passthrough, is_non_alnum, dry_run,
                            filter_this, focus_widget))
    return filter_this

其中:

  • _releaseevents_to_pass:每次按下一个键,都会将 Event 添加其中,等到键盘抬起时消费
    • 问题:作用是什么,应该时用于去重的

单独的按键时怎么汇集成命令的?

这里有一个误解,我老想着的是按下 ":" 输入命令的地方。

实际上这里驱动的也包括普通模式,也就是 hjkl 那些快捷键。

而按下 ":" 输入命令的地方,应该是有一个组件负责接收。

Parsers

基类是 BaseKeyParser,派生出:

  • CommandKeyParser
  • HintKeyParser

Mode 与 Parser 映射

位于 init 方法中:

def init(win_id: int, parent: QObject) -> 'ModeManager':
    """Initialize the mode manager and the keyparsers for the given win_id."""
    commandrunner = runners.CommandRunner(win_id)

    modeman = ModeManager(win_id, parent)
    objreg.register('mode-manager', modeman, scope='window', window=win_id)

    hintmanager = hints.HintManager(win_id, parent=parent)
    objreg.register('hintmanager', hintmanager, scope='window',
                    window=win_id, command_only=True)
    modeman.hintmanager = hintmanager

    log_sensitive_keys = 'log-sensitive-keys' in objects.debug_flags

    keyparsers: ParserDictType = {
        usertypes.KeyMode.normal:
            modeparsers.NormalKeyParser(
                win_id=win_id,
                commandrunner=commandrunner,
                parent=modeman),

        usertypes.KeyMode.hint:
            modeparsers.HintKeyParser(
                win_id=win_id,
                commandrunner=commandrunner,
                hintmanager=hintmanager,
                parent=modeman),

        usertypes.KeyMode.insert:
            modeparsers.CommandKeyParser(
                mode=usertypes.KeyMode.insert,
                win_id=win_id,
                commandrunner=commandrunner,
                parent=modeman,
                passthrough=True,
                do_log=log_sensitive_keys,
                supports_count=False),

        usertypes.KeyMode.passthrough:
            modeparsers.CommandKeyParser(
                mode=usertypes.KeyMode.passthrough,
                win_id=win_id,
                commandrunner=commandrunner,
                parent=modeman,
                passthrough=True,
                do_log=log_sensitive_keys,
                supports_count=False),

        usertypes.KeyMode.command:
            modeparsers.CommandKeyParser(
                mode=usertypes.KeyMode.command,
                win_id=win_id,
                commandrunner=commandrunner,
                parent=modeman,
                passthrough=True,
                do_log=log_sensitive_keys,
                supports_count=False),

        usertypes.KeyMode.prompt:
            modeparsers.CommandKeyParser(
                mode=usertypes.KeyMode.prompt,
                win_id=win_id,
                commandrunner=commandrunner,
                parent=modeman,
                passthrough=True,
                do_log=log_sensitive_keys,
                supports_count=False),

        usertypes.KeyMode.yesno:
            modeparsers.CommandKeyParser(
                mode=usertypes.KeyMode.yesno,
                win_id=win_id,
                commandrunner=commandrunner,
                parent=modeman,
                supports_count=False),

        usertypes.KeyMode.caret:
            modeparsers.CommandKeyParser(
                mode=usertypes.KeyMode.caret,
                win_id=win_id,
                commandrunner=commandrunner,
                parent=modeman,
                passthrough=True),

        usertypes.KeyMode.set_mark:
            modeparsers.RegisterKeyParser(
                mode=usertypes.KeyMode.set_mark,
                win_id=win_id,
                commandrunner=commandrunner,
                parent=modeman),

        usertypes.KeyMode.jump_mark:
            modeparsers.RegisterKeyParser(
                mode=usertypes.KeyMode.jump_mark,
                win_id=win_id,
                commandrunner=commandrunner,
                parent=modeman),

        usertypes.KeyMode.record_macro:
            modeparsers.RegisterKeyParser(
                mode=usertypes.KeyMode.record_macro,
                win_id=win_id,
                commandrunner=commandrunner,
                parent=modeman),

        usertypes.KeyMode.run_macro:
            modeparsers.RegisterKeyParser(
                mode=usertypes.KeyMode.run_macro,
                win_id=win_id,
                commandrunner=commandrunner,
                parent=modeman),
    }

    for mode, parser in keyparsers.items():
        modeman.register(mode, parser)

    return modeman

快捷键的注册表在哪里

研究了那么多,但是没看到哪些命令是怎么与案件关联起来的。

找了一圈让我找到了,原来在 qutebrowser\config\configdata.yml 里,通过配置文件来声明的。

在 qutebrowser\config\configdata.py 中会加载该配置文件:

def init() -> None:
    """Initialize configdata from the YAML file."""
    global DATA, MIGRATIONS
    DATA, MIGRATIONS = _read_yaml(resources.read_file('config/configdata.yml'))

总结

本文对 QuteBrowser 中键盘驱动系统的重点模块做了分析,但是很多地方还没有讲透。

比如各个模式作用是什么。另外也感觉键盘驱动系统跟 Mode 管理还不是一回事,可能需要拆除去一篇单独文章。

待后续持续完善。